Code Coverage
 
Classes and Traits
Functions and Methods
Lines
Total
0.00% covered (danger)
0.00%
0 / 1
41.67% covered (danger)
41.67%
5 / 12
CRAP
77.62% covered (warning)
77.62%
111 / 143
ProductModelUpdater
0.00% covered (danger)
0.00%
0 / 1
41.67% covered (danger)
41.67%
5 / 12
86.68
77.62% covered (warning)
77.62%
111 / 143
 __construct
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
7 / 7
 update
0.00% covered (danger)
0.00%
0 / 1
4.32
72.73% covered (warning)
72.73%
8 / 11
 filterData
100.00% covered (success)
100.00%
1 / 1
3
100.00% covered (success)
100.00%
6 / 6
 setData
100.00% covered (success)
100.00%
1 / 1
7
100.00% covered (success)
100.00%
22 / 22
 filterParentAssociations
0.00% covered (danger)
0.00%
0 / 1
3.07
80.00% covered (warning)
80.00%
4 / 5
 validateScalar
0.00% covered (danger)
0.00%
0 / 1
3.33
66.67% covered (warning)
66.67%
2 / 3
 validateScalarArray
0.00% covered (danger)
0.00%
0 / 1
10.40
40.00% covered (danger)
40.00%
4 / 10
 updateParentAndFamily
100.00% covered (success)
100.00%
1 / 1
5
100.00% covered (success)
100.00%
7 / 7
 updateParent
0.00% covered (danger)
0.00%
0 / 1
13.70
61.29% covered (warning)
61.29%
19 / 31
 updateFamilyVariant
0.00% covered (danger)
0.00%
0 / 1
8
95.65% covered (success)
95.65%
22 / 23
 validateAssociationsDataType
0.00% covered (danger)
0.00%
0 / 1
8.12
50.00% covered (danger)
50.00%
8 / 16
 updateProductModelFields
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
2 / 2
<?php
declare(strict_types=1);
namespace Akeneo\Pim\Enrichment\Component\Product\Updater;
use Akeneo\Pim\Enrichment\Component\Product\Association\ParentAssociationsFilter;
use Akeneo\Pim\Enrichment\Component\Product\Model\ProductModelInterface;
use Akeneo\Tool\Component\StorageUtils\Exception\ImmutablePropertyException;
use Akeneo\Tool\Component\StorageUtils\Exception\InvalidObjectException;
use Akeneo\Tool\Component\StorageUtils\Exception\InvalidPropertyException;
use Akeneo\Tool\Component\StorageUtils\Exception\InvalidPropertyTypeException;
use Akeneo\Tool\Component\StorageUtils\Exception\UnknownPropertyException;
use Akeneo\Tool\Component\StorageUtils\Repository\IdentifiableObjectRepositoryInterface;
use Akeneo\Tool\Component\StorageUtils\Updater\ObjectUpdaterInterface;
use Akeneo\Tool\Component\StorageUtils\Updater\PropertySetterInterface;
use Doctrine\Common\Util\ClassUtils;
/**
 * @author    Arnaud Langlade <arnaud.langlade@akeneo.com>
 * @copyright 2017 Akeneo SAS (http://www.akeneo.com)
 * @license   http://opensource.org/licenses/osl-3.0.php  Open Software License (OSL 3.0)
 */
class ProductModelUpdater implements ObjectUpdaterInterface
{
    /** @var PropertySetterInterface */
    private $propertySetter;
    /** @var ObjectUpdaterInterface */
    private $valuesUpdater;
    /** @var array */
    private $ignoredFields;
    /** @var IdentifiableObjectRepositoryInterface */
    private $familyVariantRepository;
    /** @var IdentifiableObjectRepositoryInterface */
    private $productModelRepository;
    /** @var ParentAssociationsFilter */
    private $parentAssociationsFilter;
    /**
     * @param PropertySetterInterface               $propertySetter
     * @param ObjectUpdaterInterface                $valuesUpdater
     * @param IdentifiableObjectRepositoryInterface $familyVariantRepository
     * @param IdentifiableObjectRepositoryInterface $productModelRepository
     * @param ParentAssociationsFilter              $parentAssociationsFilter
     * @param array                                 $ignoredFields
     */
    public function __construct(
        PropertySetterInterface $propertySetter,
        ObjectUpdaterInterface $valuesUpdater,
        IdentifiableObjectRepositoryInterface $familyVariantRepository,
        IdentifiableObjectRepositoryInterface $productModelRepository,
        ParentAssociationsFilter $parentAssociationsFilter,
        array $ignoredFields
    ) {
        $this->propertySetter = $propertySetter;
        $this->valuesUpdater = $valuesUpdater;
        $this->familyVariantRepository = $familyVariantRepository;
        $this->productModelRepository = $productModelRepository;
        $this->ignoredFields = $ignoredFields;
        $this->parentAssociationsFilter = $parentAssociationsFilter;
    }
    /**
     * {@inheritdoc}
     */
    public function update($productModel, array $data, array $options = [])
    {
        if (!$productModel instanceof ProductModelInterface) {
            throw InvalidObjectException::objectExpected(
                ClassUtils::getClass($productModel),
                ProductModelInterface::class
            );
        }
        if (array_key_exists('parent', $data)) {
            $this->updateParentAndFamily($productModel, $data);
            unset($data['parent']);
        }
        foreach ($data as $code => $value) {
            $filteredValue = $this->filterData($code, $value, $data);
            $this->setData($productModel, $code, $filteredValue, $options);
        }
        return $this;
    }
    /**
     * @param                  $field
     * @param                  $data
     * @param array            $context
     * @return array
     */
    protected function filterData($field, $data, array $context = [])
    {
        switch ($field) {
            case 'associations':
                $this->validateAssociationsDataType($data);
                if (isset($context['parent_associations'])) {
                    $data = $this->filterParentAssociations($data, $context['parent_associations']);
                }
                break;
        }
        return $data;
    }
    /**
     * @param ProductModelInterface $productModel
     * @param                       $field
     * @param                       $data
     * @param array                 $options
     */
    protected function setData(ProductModelInterface $productModel, $field, $data, $options = [])
    {
        switch ($field) {
            case 'values':
                $this->valuesUpdater->update($productModel, $data, $options);
                break;
            case 'code':
                $this->validateScalar($field, $data);
                $productModel->setCode($data);
                break;
            case 'family_variant':
                $this->validateScalar($field, $data);
                $this->updateFamilyVariant($productModel, $data);
                break;
            case 'categories':
                $this->validateScalarArray($field, $data);
                $this->propertySetter->setData($productModel, $field, $data);
                break;
            case 'associations':
                $this->validateAssociationsDataType($data);
                $this->updateProductModelFields($productModel, $field, $data);
                break;
            default:
                if (!in_array($field, $this->ignoredFields)) {
                    throw UnknownPropertyException::unknownProperty($field);
                }
        }
    }
    /**
     * Validate association data
     *
     * @param array $associations
     * @param array $parentAssociations
     *
     * @return array
     */
    protected function filterParentAssociations(array $associations, ?array $parentAssociations): array
    {
        if (null === $parentAssociations) {
            return $associations;
        }
        $associations = $this->parentAssociationsFilter->filterParentAssociations(
            $associations,
            $parentAssociations
        );
        return $associations;
    }
    /**
     * Validate that it is a scalar value.
     *
     * @param $field
     * @param $data
     *
     * @throws InvalidPropertyTypeException
     */
    private function validateScalar($field, $data)
    {
        if (null !== $data && !is_scalar($data)) {
            throw InvalidPropertyTypeException::scalarExpected($field, static::class, $data);
        }
    }
    /**
     * Validate that it is an array with scalar values.
     *
     * @param string $field
     * @param mixed $data
     *
     * @throws InvalidPropertyTypeException
     */
    private function validateScalarArray($field, $data)
    {
        if (!is_array($data)) {
            throw InvalidPropertyTypeException::arrayExpected($field, static::class, $data);
        }
        foreach ($data as $value) {
            if (null !== $value && !is_scalar($value)) {
                throw InvalidPropertyTypeException::validArrayStructureExpected(
                    $field,
                    sprintf('one of the %s is not a scalar', $field),
                    static::class,
                    $data
                );
            }
        }
    }
    /**
     * As for a sub product model, the family variant can be guessed from the
     * parent, it needs to be set first, then the family.
     * However, the family variant will be automatically set only if there is not
     * already one and if the product model has a parent.
     *
     * @param ProductModelInterface $productModel
     * @param array                 $data
     */
    private function updateParentAndFamily(ProductModelInterface $productModel, array $data): void
    {
        $this->updateParent($productModel, $data['parent']);
        if (null === $productModel->getFamilyVariant() &&
            (!isset($data['family_variant']) || '' === $data['family_variant'])
        ) {
            $parent = $productModel->getParent();
            if (null !== $parent) {
                $productModel->setFamilyVariant($parent->getFamilyVariant());
            }
        }
    }
    /**
     * Updates the parent of the product model.
     * If the product model is a root one, it cannot have a parent, so an
     * exception will be thrown.
     *
     * @param ProductModelInterface $productModel
     * @param string|null                $parentCode
     *
     * @throws ImmutablePropertyException
     * @throws InvalidPropertyException
     */
    private function updateParent(ProductModelInterface $productModel, ?string $parentCode): void
    {
        if (empty($parentCode)) {
            return;
        }
        if (null !== $productModel->getId() && $productModel->isRootProductModel()) {
            throw ImmutablePropertyException::immutableProperty(
                'parent',
                $parentCode,
                static::class
            );
        }
        $newParentModel = $this->productModelRepository->findOneByIdentifier($parentCode);
        if (null === $newParentModel) {
            throw InvalidPropertyException::validEntityCodeExpected(
                'parent',
                'parent code',
                'The product model does not exist',
                static::class,
                $parentCode
            );
        }
        if (!$newParentModel->isRootProductModel()) {
            throw InvalidPropertyException::validEntityCodeExpected(
                'parent',
                'parent code',
                'The new parent of the product model must be a root product model',
                static::class,
                $parentCode
            );
        }
        if ($productModel->getFamilyVariant() !== null &&
           $productModel->getFamilyVariant()->getCode() !== $newParentModel->getFamilyVariant()->getCode()
        ) {
            throw InvalidPropertyException::validEntityCodeExpected(
                'parent',
                'parent code',
                'The new parent of the product model must be of the same family variant',
                static::class,
                $parentCode
            );
        }
        $productModel->setParent($newParentModel);
    }
    /**
     * Updates the family variant of the family variant of the product model.
     *
     * @param ProductModelInterface $productModel
     * @param string                $familyVariantCode
     *
     * @throws ImmutablePropertyException
     * @throws InvalidPropertyException
     */
    private function updateFamilyVariant(ProductModelInterface $productModel, string $familyVariantCode): void
    {
        if (empty($familyVariantCode) && null !== $productModel->getFamilyVariant()) {
            return;
        }
        if (null !== $productModel->getFamilyVariant() &&
            $familyVariantCode !== $productModel->getFamilyVariant()->getCode()
        ) {
            throw ImmutablePropertyException::immutableProperty(
                'family_variant',
                $familyVariantCode,
                static::class
            );
        }
        $parent = $productModel->getParent();
        if (null !== $parent && $familyVariantCode !== $parent->getFamilyVariant()->getCode()) {
            throw InvalidPropertyException::expected(
                sprintf(
                    'The parent is not a product model of the family variant "%s" but belongs to the family "%s".',
                    $familyVariantCode,
                    $parent->getFamilyVariant()->getCode()
                ),
                static::class
            );
        }
        if (null === $familyVariant = $this->familyVariantRepository->findOneByIdentifier($familyVariantCode)) {
            throw InvalidPropertyException::validEntityCodeExpected(
                'family_variant',
                'family variant code',
                'The family variant does not exist',
                static::class,
                $familyVariantCode
            );
        }
        $productModel->setFamilyVariant($familyVariant);
    }
    /**
     * Validate association data
     *
     * @param $data
     *
     * @throws InvalidPropertyTypeException
     */
    protected function validateAssociationsDataType($data): void
    {
        if (!is_array($data)) {
            throw InvalidPropertyTypeException::arrayExpected(
                'associations',
                static::class,
                $data
            );
        }
        foreach ($data as $associationTypeCode => $associationTypeValues) {
            $this->validateScalar('associations', $associationTypeCode);
            if (!is_array($associationTypeValues)) {
                throw InvalidPropertyTypeException::arrayExpected(
                    'associations',
                    static::class,
                    $associationTypeValues
                );
            }
            foreach ($associationTypeValues as $property => $value) {
                $this->validateScalar('associations', $property);
                $this->validateScalarArray('associations', $value);
            }
        }
    }
    /**
     * Sets the field
     *
     * @param ProductModelInterface $productModel
     * @param string                $field
     * @param mixed                 $value
     */
    protected function updateProductModelFields(ProductModelInterface $productModel, $field, $value): void
    {
        $this->propertySetter->setData($productModel, $field, $value);
    }
}